参考:

ReactiveCocoaTutorial-part1

ReactiveCocoaTutorial-part2

http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/

https://github.com/ReactiveCocoa/ReactiveCocoa/tree/master/Documentation

MVVM

注意

1
2
3
4
5
View引用了ViewModel,但反过来不行。也就是说ViewController知道ViewModel,但是ViewModel不知道ViewController。如果要更新View上的显示状态,通过数据绑定的方式监听变更。

ViewModel引用了Model,但反过来不行。ViewModel暴露用户交互的接口给View,接受View发出的命令,执行网络请求,数据存储的操作。

如果我们破坏了这些规则,便无法正确地使用MVVM。

MVVM是MVC模式的一个变种,它正逐渐流行起来, MVVM模式让View层代码变得更清晰,更易于测试, 严格遵守View=>ViewModel=>Model这样一个引用层次,然后通过绑定来将ViewModel的更新反映到View层上。ViewModel层决不应该维护View的引用, ViewModel层可以看作是视图的模型(model-of-the-view),它暴露属性,以直接反映视图的状态,以及执行用户交互相关的命令。 Model层暴露服务。 针对MVVM程序的测试可以在没有UI的情况下运行。

  • view :由 MVC 中的 view 和 controller 组成,负责 UI 的展示,绑定 viewModel 中的属性,触发 viewModel 中的命令;

  • viewModel :从 MVC 的 controller 中抽取出来的展示逻辑,负责从 model 中获取 view 所需的数据,转换成 view 可以展示的数据,并暴露公开的属性和命令供 view 进行绑定;

  • model :与 MVC 中的 model 一致,包括数据模型、访问数据库的操作和网络请求等;

  • binder :在 MVVM 中,声明式的数据和命令绑定是一个隐含的约定,它可以让开发者非常方便地实现 view 和 viewModel 的同步,避免编写大量繁杂的样板化代码。

ReactiveCocoa

MVVM模式依赖于数据绑定,它是一个框架级别的特性,用于自动连接对象属性和UI控件。iOS没有数据绑定框架,幸运的是我们可以通过ReactiveCocoa来实现这一功能。我们从iOS开发的角度来看看MVVM模式,ViewController及其相关的UI(nib, stroyboard或纯代码的View)组成了View。

ViewModel暴露属性(RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;)来表示UI状态,它同样暴露命令(RACCommand)来表示UI操作(通常是方法)。ViewModel负责管理基于用户交互的UI状态的改变。然而它不负责实际执行这些交互产生的的业务逻辑,那是Model的工作。

类型

  • RACSignal

信号,带着参数等信息。作为基本元素在传递链中传递。 尽管每个订阅信号的Subscription可以指定自己的执行在哪个线程,但是RAC保证信号是串行的。也就是说一定要等信号处理完了,才会往下传递

  • Subjects

Subject也是个Signal。不过是Mutable的Signal

  • RACSequence

RACSequence 是一种集合。Cocoa框架的大多数集合类型,RAC都有提供rac_sequence方法,以使用RACSequence。Sequence用的是懒加载,访问的时候才创建。下面例子中 sequence.head访问的时候才调用 map的block。输出A, 赋值给concatA 为 A_

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NSArray *strings = @[ @"A", @"B", @"C" ];
RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) {
NSLog(@"%@", str);
return [str stringByAppendingString:@"_"];
}];

// Logs "A" during this call.
NSString *concatA = sequence.head;

// Logs "B" during this call.
NSString *concatB = sequence.tail.head;

// Does not log anything.
NSString *concatB2 = sequence.tail.head;

RACSequence *derivedSequence = [sequence map:^(NSString *str) {
return [@"_" stringByAppendingString:str];
}];

// Still does not log anything, because "B_" was already evaluated, and the log
// statement associated with it will never be re-executed.
NSString *concatB3 = derivedSequence.tail.head;


输出:
A
B

信号创建

  • RACObserve创建类似 kvo 监控变量的信号

searchText 是 self的变量。 searchText变化,会触发 订阅者subscribeNext的block代码NSLog(@"%@", text);

1
2
3
[RACObserve(self, searchText) subscribeNext:^(NSString *text) {
NSLog(@"%@", text);
}];
  • RACCommand 命令。 RACComand 创建一个响应 UI 交互的 信号。下面的例子是:每当按钮点击,就调用 block
1
2
3
4
5
6
当button按下的时候,打印 button was pressed 。 rac_command 是 button的扩展。 每次按下button,都会发送响应信号。

self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
NSLog(@"button was pressed!");
return [RACSignal empty];
}];

下面例子的 RACCommand 等待着 validSearchSignal 信号发出。相当于订阅了 validSearchSignal , validSearchSignal 的信号带参为YES,则下发 [self executeSearchSignal]; 这个信号。执行搜索

1
2
3
4
self.executeSearch = [[RACCommand alloc] initWithEnabled:validSearchSignal
signalBlock:^RACSignal *(id input) {
return [self executeSearchSignal];
}];

用户点击login按钮,触发 [clinet logIn]。self.loginCommand.executionSignals 相当于 self.loginCommand的信号,可以继续增加订阅者,传递信号。 所以是 用户点击 login button -> return [client logIn]; ->
subscribeNext:^(RACSignal *loginSignal) { ... }]; -> 执行 [loginSignal subscribeCompleted:^{ NSLog(@"Logged in successfully!");}]; -> 等待网络请求完毕,下发loginSignal(不带参数) -> NSLog(@"Logged in successfully!");

1
2
3
4
5
6
7
8
9
10
11
12
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) {
return [client logIn];
}];

[self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[loginSignal subscribeCompleted:^{
NSLog(@"Logged in successfully!");
}];
}];

//当loginButton按下,会发送 rac_command. 触发 rac_command的订阅者
self.loginButton.rac_command = self.loginCommand;
  • [RACSignal createSignal:subscribeOn:] 创建网络请求返回结果后发出信号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* 
这个block的返回值是一个RACDisposable对象,它允许你在一个订阅被取消时执行一些清理工作。当前的信号不需要执行清理操作,所以返回nil就可以了
block的入参是一个subscriber实例,它遵循RACSubscriber协议,协议里有一些方法来产生事件,你可以发送任意数量的next事件,或者用error\complete事件来终止。本例中,信号发送了一个next事件来表示同意访问,随后是一个complete事件
*/
- (RACSignal *)requestAccessToTwitterSignal {
// 1 - define an error
NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain
code:RWTwitterInstantErrorAccessDenied
userInfo:nil];

// 2 - create the signal @weakify宏让你创建一个弱引用的影子对象
@weakify(self)
return [RACSignal createSignal:
^RACDisposable *(id subscriber) {
// 3 - request access to twitter @strongify让你创建一个对之前传入@weakify对象的强引用。
@strongify(self)
NSLog(@"request signal");
[self.accountStore requestAccessToAccountsWithType:self.twitterAccountType
options:nil
completion:^(BOOL granted, NSError *error) {
// 4 - handle the response
if (!granted) {
[subscriber sendError:accessError];
} else {
[subscriber sendNext:nil];
[subscriber sendCompleted];
}
}];
return nil;
}
];
}

信号传递流程中的一些处理

  • subscribeNext、subscribeComplete、subscribeError

用于访问信号中的值

1
2
3
4
5
6
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

// Outputs: A B C D E F G H I
[letters subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
  • filter

过滤传入值 filter。 filter会过滤信号,block返回YES的才会继续下发

1
2
3
4
5
6
7
8
9
[RACObserve(self, searchText) 
filter:^(NSString *text) {
        return @(text.length > 3);
    }]

subscribeNext:^(NSString *text) {
NSLog(@"%@", text);
}
];
1
2
3
4
5
6
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: 2 4 6 8
RACSequence *filtered = [numbers filter:^ BOOL (NSString *value) {
return (value.intValue % 2) == 0;
}];
  • map

改变信号带的参数。 map, 这里的 map 把 入参 NSString *text 转变为 id x 。用宏 RACObserve监测 NSString类型的 searchText。一旦 searchText的值发生变化,就发送信号。distinctUntilChanged 确保信号的状态有改变,才继续下发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//使用RACObserve宏来从ViewModel的searchText属性创建一个信号。一旦searchText有变化,就发出信号
// searchText 是 self的变量。
RACSignal *validSearchSignal =
[[RACObserve(self, searchText)
//map操作将文本转化为一个true或false值的流。
map:^id(NSString *text) {
return @(text.length > 3);
}]

//最后,distinctUntilChanges确保信号只有在状态改变时才发出
distinctUntilChanged];

[validSearchSignal subscribeNext:^(id x) {
NSLog(@"search text is valid %@", x);
}];
1
2
3
4
5
6
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;

// Contains: AA BB CC DD EE FF GG HH II
RACSequence *mapped = [letters map:^(NSString *value) {
return [value stringByAppendingString:value];
}];
  • flattenMap

    把block的信号转换替换为了源信号,同时还从内部信号发送事件到外部信号,使得信号继续传递下去。例如下面的例子, logInUser是个信号,发出信号后,往下传递。 flattenMap接收到信号后,调用[client loadCachedMessagesForUser:user]返回另外一个信号,替换原来的信号继续传递下去。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [[[[client 
    logInUser]
    flattenMap:^(User *user) {
    // Return a signal that loads cached messages for the user.
    return [client loadCachedMessagesForUser:user];
    }]
    flattenMap:^(NSArray *messages) {
    // Return a signal that fetches any remaining messages.
    return [client fetchMessagesAfterMessage:messages.lastObject];
    }]
    subscribeNext:^(NSArray *newMessages) {
    NSLog(@"New messages: %@", newMessages);
    } completed:^{
    NSLog(@"Fetched all messages.");
    }];
  • then

等待 completed事件发射后,才调用 block里面返回的signal。也就是阻断了 next的事件传递。只有complete,才能继续往下传信号。而且是信号还被转换了。 如果信号发出的是error,不会被then阻断,会直接调用订阅者的 error block。

  • throttle

只有当前一个next事件在指定的时间段内没有被接收到后,throttle操作才会发送next事件。 避免在一段时间内反复发送很多信号

  • deliverOn

    切换到主线程,用于更新UI。注意:如果你看一下RACScheduler类,就能发现还有很多选项,比如不同的线程优先级,或者在管道中添加延迟。

  • doNext、doError、doCompleted 把block注入对应的订阅者。本身并不去订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__block unsigned subscriptions = 0;

RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
subscriptions++;
[subscriber sendCompleted];
return nil;
}];

// Does not output anything yet
loggingSignal = [loggingSignal doCompleted:^{
NSLog(@"about to complete subscription %u", subscriptions);
}];

// Outputs:
// about to complete subscription 1
// subscription 1
[loggingSignal subscribeCompleted:^{
NSLog(@"subscription %u", subscriptions);
}];

综合例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void)twitterPipe{
/*
一旦用户允许访问Twitter账号(希望如此),应用就应该一直监测search text filed的变化,以便搜索Twitter的内容。
应用应该等待获取访问Twitter权限的signal发送completed事件,然后再订阅text field的signal。按顺序链接不同的signal是一个常见的问题,ReactiveCocoa处理的很好。

requestAccessToTwitterSignal -> error -> subscribeNext:error 调用error。
else
requestAccessToTwitterSignal -> complete -> then 返回 searchText.rac_textSignal ,【转换成了 searchText.rac_textSignal的信号】,data 是NSString的text -> filter data是Bool 返回yes的话就 -> subscribeNext data是 UITextField的text。

如果中间没有 then的对信号的偷换,是会调用 subscribeNext 跟 complete的。

*/
@weakify(self)
[[[[[[[self requestAccessToTwitterSignal]
then:^RACSignal *{
@strongify(self)
return self.searchText.rac_textSignal;
}]

filter:^BOOL(NSString *text) {
@strongify(self)
return [self isValidSearchText:text];
}]

//只有当,前一个next事件在指定的时间段内没有被接收到后,throttle操作才会发送next事件。 避免在一段时间内反复发送很多信号
throttle:0.5]

flattenMap:^RACStream *(NSString *text) {
@strongify(self)
return [self signalForSearchWithText:text];
}]

//切换到主线程,用于更新UI。注意:如果你看一下RACScheduler类,就能发现还有很多选项,比如不同的线程优先级,或者在管道中添加延迟。
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(NSDictionary *jsonSearchResult) {
NSArray *statuses = jsonSearchResult[@"statuses"];
NSArray *tweets = [statuses linq_select:^id(id tweet) {
return [RWTweet tweetWithStatus:tweet];
}];
[self.resultsViewController displayTweets:tweets];
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
}

切换线程

通过调用 deliverOn:[RACScheduler mainThreadScheduler]] 使得订阅者在主线程运行

1
2
3
4
5
6
7
8
9
10
[[[[self signalForLoadingImage:tweet.profileImageUrl]

takeUntil:cell.rac_prepareForReuseSignal]

deliverOn:[RACScheduler mainThreadScheduler]]

subscribeNext:^(UIImage *image) {
cell.twitterAvatarView.image = image;
}
];

[RACSignal createSignal:subscribeOn:scheduler]在创建信号的时候,调用多一步 scheduler, 来指定信号源在哪个线程执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {

/*
首先获取一个后台scheduler,来让signal不在主线程执行。然后,创建一个signal来下载图片数据,当有订阅者时创建一个UIImage。最后是subscribeOn:来确保signal在指定的scheduler上执行。
*/
RACScheduler *scheduler = [RACScheduler
schedulerWithPriority:RACSchedulerPriorityBackground];

return [[RACSignal createSignal:^RACDisposable *(id subscriber) {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
UIImage *image = [UIImage imageWithData:data];
[subscriber sendNext:image];
[subscriber sendCompleted];
return nil;
}] subscribeOn:scheduler];

}

避免block循环引用

@weakify宏让你创建一个弱引用的影子对象,@strongify(self)对这个影子对象的引用。

1
2
3
4
5
6
7
 
// 2 - create the signal block
@weakify(self)
return [RACSignal createSignal:^RACDisposable *(id subscriber) {
@strongify(self);
···
}];

把 @protocol 定义的传统委托方法转换成RAC的写法,以及一些其他异步操作的转换

有代理 @protocol OFFlickrAPIRequestDelegate <NSObject>方法 - (void)flickrAPIRequest:(OFFlickrAPIRequest *)inRequest didCompleteWithResponse:(NSDictionary *)inResponseDictionary;。可以这样来通过代理创建信号,

1
2
3
// 3. 从代理方法中创建一个信号。rac_signalForSelector:fromProtocol: 方法创建了successSignal,同样也在代理方法的调用中创建了信号。
RACSignal *successSignal = [self rac_signalForSelector:@selector(flickrAPIRequest:didCompleteWithResponse:)
fromProtocol:@protocol(OFFlickrAPIRequestDelegate)];

然后 代理中的参数都放在了RACTuple 这个类中。tuple.second的意思是第二个参数的意思

1
2
3
4
5
6
7
8
9
10
11
12
13
// 4. 处理响应
[[[successSignal
//代理方法每次调用时,发出的next事件会附带包含方法参数的RACTuple
map:^id(RACTuple *tuple) {
return tuple.second;
}]

map:block]

subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
}];

其他的异步行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 代理方法
[[self
rac_signalForSelector:@selector(webViewDidStartLoad:)
fromProtocol:@protocol(UIWebViewDelegate)]
subscribeNext:^(id x) {
// 实现 webViewDidStartLoad: 代理方法
}];

// target-action
[[self.avatarButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(UIButton *avatarButton) {
// avatarButton 被点击了
}];

// 通知
[[[NSNotificationCenter defaultCenter]
rac_addObserverForName:kReachabilityChangedNotification object:nil]
subscribeNext:^(NSNotification *notification) {
// 收到 kReachabilityChangedNotification 通知
}];

// KVO
[RACObserve(self, username) subscribeNext:^(NSString *username) {
// 用户名发生了变化
}];

信号延迟,间隔时间内给机会反悔做决定是否发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 不用这个方法为了节流
RACSignal *fetchMetadata =
[RACObserve(self, isVisible)
filter:^BOOL(NSNumber *visible) {
return [visible boolValue];
}]; */

// 1. 通过监听isVisible属性来创建信号。该信号发出的第一个next事件将包含这个属性的初始状态。
// 因为我们只关心这个值的改变,所以在第一个事件上调用skip操作。
RACSignal *visibleStateChanged = [RACObserve(self, isVisible) skip:1];

// 2. 通过过滤visibleStateChanged信号来创建一个信号对,一个标识从可见到隐藏的转换,另一个标识从隐藏到可见的转换
RACSignal *visibleSignal = [visibleStateChanged filter:^BOOL(NSNumber *value) {
return [value boolValue];
}];

RACSignal *hiddenSignal = [visibleStateChanged filter:^BOOL(NSNumber *value) {
return ![value boolValue];
}];

// 3. 这里是最神奇的地方。通过延迟visibleSignal信号1秒钟来创建fetchMetadata信号,在获取元数据之前暂停一会。
// takeUntil操作确保如果cell在1秒的时间间隔内又一次隐藏时,来自visibleSignal的next事件被挂起且不获取元数据。
RACSignal *fetchMetadata = [[visibleSignal delay:1.0f]
takeUntil:hiddenSignal];

多个信号合并成一个信号

  • combileLatest 合并信号给其他信号赋值。 例如下面的例子, 给self name1 和 sel name2 变化后,就会触发 reduce的block运行,判断name1 与 name2 是否相等。赋值给 self isSomeName。
1
2
3
4
5
RAC(self, isSomeName) = [RACSignal 
combineLatest:@[ RACObserve(self, name1), RACObserve(self, name2) ]
reduce:^(NSString *name1, NSString *name2) {
return @([name1 isEqualToString:name2]);
}];
  • combineLatest 下面的例子是 合并 favorites(信号带参NSString favs) 和 comments(信号带参NSString coms) 返回值后(都发出了信号)。触发 reduce block订阅者。用两个源订阅的数据合并成一个 RWTFlickrPhotoMetadata的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (RACSignal *)flickrImageMetadata:(NSString *)photoId {

//请求收藏数
RACSignal *favorites = [self signalFromAPIMethod:@"flickr.photos.getFavorites"
arguments:@{@"photo_id": photoId}
transform:^id(NSDictionary *response) {
NSString *total = [response valueForKeyPath:@"photo.total"];
return total;
}];

//请求评论数
RACSignal *comments = [self signalFromAPIMethod:@"flickr.photos.getInfo"
arguments:@{@"photo_id": photoId}
transform:^id(NSDictionary *response) {
NSString *total = [response valueForKeyPath:@"photo.comments._text"];
return total;
}];

//一旦创建了两个信号,combineLatest:reduce:方法生成一个新的信号来组合两者。
//这个方法等待源信号的一个next事件。reduce块使用它们的内容来调用,其结果变成联合信号的next事件。
return [RACSignal combineLatest:@[favorites, comments]
reduce:^id(NSString *favs, NSString *coms){
RWTFlickrPhotoMetadata *meta = [RWTFlickrPhotoMetadata new];
meta.comments = [coms integerValue];
meta.favorites = [favs integerValue];
return meta;
}
];
}
  • merge , 合并两个信号,当两个信号都完成了后触发。 也就是两个信号都 发送了 compeleted 信号后
1
2
3
4
5
[[RACSignal 
merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
subscribeCompleted:^{
NSLog(@"They're both done!");
}];
  • concat:

链接两个流的带的参数值

1
2
3
4
5
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *concatenated = [letters concat:numbers];
  • flatten

合并两个流所带的值

序列:

1
2
3
4
5
6
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *sequenceOfSequences = @[ letters, numbers ].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *flattened = [sequenceOfSequences flatten];

信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:letters];
[subscriber sendNext:numbers];
[subscriber sendCompleted];
return nil;
}];

RACSignal *flattened = [signalOfSignals flatten];

// Outputs: A 1 B C 2
[flattened subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];